浅谈 iOS Device ID 的修改
最近有一篇 文章 介绍了如何实现 AppStore App 自动下载,笔者看后收获良多。不过文中只介绍了如何去模拟用户的操作来完成下载,并没有涉及抹机、IP 更换等内容。所以笔者打算在此分享一下自己对这些方面的经验。
##FBI WARNING
以下内容可能会引起很多人不适,请读者自酌。
18 岁以下请在家长陪同下观看!
部分内容可能违反你所在地相关法律,请谨慎模仿
为什么要修改 iOS Device ID ? 修改设备唯一可识别标识可以做很多事前,比如防止根据 UUID 的追踪,避免大数据「杀熟」等。但是在 iOS 设备上目前想做到修改的前提是越狱,所以为了多领几个美团红包而选择承担越狱的风险,是否值得还是要考虑清楚的。 不过在业界有大量应用这种技术的产业,比如积分墙、ASO 刷榜…… 不过这些产业就属于「灰黑产」了,涉及到了原力的黑暗面,所以笔者不建议涉世不深的读者继续阅读下去。
当你凝视深渊,深渊也在凝视着你。
现状 在开始讲如何做之前,笔者决定先简单介绍一下业界现在已经能做什么:
如图所示,这是一款在业内非常常见的改机软件。由于作者不可考(不过理应如此,毕竟为了自己的人生安全)、源码遗失、以及 iOS 版本的多次更新,现在已经不值钱了。但是麻雀虽小五脏俱全,它能够修改设备的五码、机型、配置 Apple ID 和一键越狱等。 前人的成功告诉了我们这是可行的,剩下的只是模仿,因此笔者深入逆向并研究了这款软件,在当我看到了一大堆用汇编写的混淆之后…… 放弃了。 所以下面的内容都是笔者编的,大家有兴趣看个开心就好,基本上可以点关闭按钮了 (●°u°●) 」
如何破解一款程序? 笔者依稀记得 狗神 在他那本著名的 小黄书 中提到,逆向一款软件最重要的不是最终成品的代码,而是过程的分析与思路。所以经常可以看到一款软件的破解代码重要的也许只有两三行,但是过程有多艰辛也许只有破解者才知道。例如破解 Mac 版 QQ 音乐下载需要 VIP 权限的限制的代码也许加上注释也不到一百行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 %config(generator = internal) #import <Foundation/Foundation.h> #include <substrate.h> %hook DownLoadTask - (BOOL )checkHaveRightToDownload:(int )argument { return YES ; } %end unsigned int (*old_GetFlexBOOL)(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8);unsigned int new_GetFlexBOOL(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8){ return 1 ; } %ctor { NSLog (@"!!!!!!inject success!!!!!!!" ); void * Symbol = MSFindSymbol(MSGetImageByName("/Applications/QQMusic.app/Contents/MacOS/QQMusic" ), "_GetFlexBOOL" ); MSHookFunction(Symbol, &new_GetFlexBOOL, (void *)&old_GetFlexBOOL); }
而真正重要的是找出思路和逆向分析的过程,操作系统本质上也是一个软件,修改 Device ID 其实和破解一款音乐 VIP 限制本质上是一样的,只是一个只需要把 checkHaveRightToDownload
的返回值改成 YES
,另一个则需要与操作系统斗智斗勇罢了。
思路 综上所述,在我们对操作系统下黑手之前应该先理清思路。顺便再说一次以下内容皆是我瞎编的,如有雷同实属巧合:
如图所示,显而易见,如果只是简简单单的修改某个 App 中用到的 Device ID,极大几率只需要勾住「再封装的私有 API」就行了。
而在众多私有 API 中,最著名的当然是大名鼎鼎的 MGCopyAnswer
。
MGCopyAnswer 1 2 3 4 CFStringRef value = MGCopyAnswer(kMGDeviceColor);NSLog (@"Value: %@" , value);CFRelease (value);
基本上平时从 UIDevice
还是其他大部分途径获取 Device ID,皆是通过调用 libMobileGestalt 中的 MGCopyAnswer
函数来获取的。所以只需要勾住 MGCopyAnswer
,使其返回的 Device ID 为我们所要的值即可,非常简单明了。
不过虽说思路很简单,但是一个萌新想要勾 MGCopyAnswer
还是会绕很多弯路的,比如最常见的就是「挂短钩」。
挂短钩 在 ARM64 架构下,直接对 MGCopyAnswer
挂钩的话会立即使进程崩溃 invalid instruction
。如果通过反汇编手段分析 libMobileGestalt 库:
1 2 01 00 80 d2 movz x1, #0 01 00 00 14 b MGCopyAnswer_internal
易知 MGCopyAnswer
实际上在内部调用了另一个私有无符号的函数 MGCopyAnswer_internal
来实现其功能。因此 MGCopyAnswer
这个函数实际上非常短,只有 8 个字节,而我们使用 Cydia Substrate 对一个 C 函数挂钩的话,它要求被勾函数至少有 16 个字节 。因此直接勾住 MGCopyAnswer
时,MGCopyAnswer
函数地址开始的 16 个字节都会被改为 goto
,从而破坏了相邻函数的前 8 个字节,使进程崩溃。 因此,当我们吭哧吭哧读完汇编之后,首先想到的方法自然是去勾这个被调用的子函数 MGCopyAnswer_internal
,虽说该函数并没有符号,但是在我们吭哧吭哧读了汇编之后,发现其函数地址与 MGCopyAnswer
相差 8 字节。故可以很简单粗暴的写出如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static CFPropertyListRef (*orig_MGCopyAnswer_internal)(CFStringRef prop, uint32_t* outTypeCode);CFPropertyListRef new_MGCopyAnswer_internal(CFStringRef prop, uint32_t* outTypeCode) { return orig_MGCopyAnswer_internal(prop, outTypeCode); } extern "C" MGCopyAnswer(CFStringRef prop);static CFPropertyListRef (*orig_MGCopyAnswer)(CFStringRef prop);CFPropertyListRef new_MGCopyAnswer(CFStringRef prop) { return orig_MGCopyAnswer(prop); } %ctor { uint8_t MGCopyAnswer_arm64_impl[8 ] = {0x01 , 0x00 , 0x80 , 0xd2 , 0x01 , 0x00 , 0x00 , 0x14 }; const uint8_t* MGCopyAnswer_ptr = (const uint8_t*) MGCopyAnswer; if (memcmp(MGCopyAnswer_ptr, MGCopyAnswer_arm64_impl, 8 ) == 0 ) { MSHookFunction(MGCopyAnswer_ptr + 8 , (void *)new_MGCopyAnswer_internal, (void **)&orig_MGCopyAnswer_internal); } else { MSHookFunction(MGCopyAnswer_ptr, (void *)new_MGCopyAnswer, (void **)&orig_MGCopyAnswer); } }
显然这段代码除了简单粗暴、没有任何框架检测与异常处理之外完美实现了挂钩任务,但是基于相对偏移量来获取函数地址也并不是很稳。
好在张总 在他的一篇博文 中提到可以使用 Capstone Engine ,一款基于 LLVM MC 的多平台多架构支持的反汇编框架来帮助我们找到 MGCopyAnswer_internal
的「符号」。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 static CFStringRef (*old_MGCA)(CFStringRef Key);CFStringRef new_MGCA(CFStringRef Key) { CFStringRef Ret = old_MGCA(Key); NSLog (@"MGHooker:%@\nReturn Value:%@" , Key, Ret); return Ret; } %ctor { void *Symbol = MSFindSymbol(MSGetImageByName("/usr/lib/libMobileGestalt.dylib" ), "_MGCopyAnswer" ); NSLog (@"MG: %p" , Symbol); csh handle; cs_insn * insn; cs_insn BLInstruction; size_t count; unsigned long realMGAddress = 0 ; if (cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &handle) == CS_ERR_OK) { count = cs_disasm(handle, (const uint8_t *)Symbol, 0x1000 , (uint64_t)Symbol, 0 , &insn); if (count > 0 ) { NSLog (@"Found %lu instructions" , count); for (size_t j = 0 ; j < count; j++) { NSLog (@"0x%" PRIx64 ":\t%s\t\t%s\n" , insn[j].address, insn[j].mnemonic, insn[j].op_str); if (insn[j].id == ARM64_INS_B) { BLInstruction = insn[j]; sscanf(BLInstruction.op_str, "#%lx" , &realMGAddress); break ; } } cs_free(insn, count); } else { NSLog (@"ERROR: Failed to disassemble given code!%i \n" , cs_errno(handle)); } cs_close(&handle); MSHookFunction((void *)realMGAddress, (void *)new_MGCA, (void **)&old_MGCA); } else { NSLog (@"MGHooker: CSE Failed" ); } }
废话不多说了,我们的正题并不在这里。
如何修改 iOS Device ID 接下来的东西我是真的就不会了,但是为了不太斧头蛇尾,我就再瞎掰一段吧。 谈到修改的话,我们首先要弄清楚的一点是我们打算要从哪一层修改?比如 ECID ,众所周知它是烧在芯片上的。讲道理的话要修改 ECID 是要对硬件动手的,但是我们一般不需要做的这么彻底,而是结合具体需求具体分析。例如一个普通、简单的积分墙,我们只需要对积分墙调用的 MGCopyAnswer
挂钩,就可以愉快的玩耍了。但是如果想对 AppStore 或者 iTunes 下手呢?自然仅仅勾个 MGCopyAnswer
是不行的。 例如我们想让手机连接 iTunes 时,iTunes 获取的 Device ID 是伪造的,那么就需要勾住处理手机与电脑间 USB 通信的守护进程——比如说 lockdownd 。因为 iTunes 并不会直接读取手机的设备信息,而是从手机上运行的守护进程中请求数据。那么我们是不是只需要在这个守护进程安装一个钩子即可?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 typedef void *LockdownConnectionRef;typedef int kern_return_t;typedef unsigned int __darwin_natural_t;typedef __darwin_natural_t __darwin_mach_port_name_t;typedef __darwin_mach_port_name_t __darwin_mach_port_t;typedef __darwin_mach_port_t mach_port_t;typedef mach_port_t io_object_t;typedef io_object_t io_registry_entry_t;typedef char io_name_t[128 ];typedef unsigned int IOOptionBits;static kern_return_t (*oldIORegistryEntryGetName)(io_registry_entry_t entry, io_name_t name);kern_return_t newIORegistryEntryGetName(io_registry_entry_t entry, io_name_t name) { int ret = oldIORegistryEntryGetName(entry, name); NSLog (@"\n\nGetName:\n\tentry:%zd\n\tio_name_t%s\n\tret:%d" , entry, name, ret); return ret; } static CFTypeRef (*oldIORegistryEntrySearchCFProperty)( io_registry_entry_t entry, const io_name_t plane, CFStringRef key, CFTypeRef allocator, IOOptionBits options); CFTypeRef newIORegistryEntrySearchCFProperty( io_registry_entry_t entry, const io_name_t plane, CFStringRef key, CFTypeRef allocator, IOOptionBits options) { CFTypeRef ret = oldIORegistryEntrySearchCFProperty(entry, plane, key, allocator, options); NSLog (@"\n\nSearchCFProperty:\n\tkey:%@\n\tret:%@\n\t%lu" , key, ret, CFGetTypeID (ret)); return ret; } static CFPropertyListRef (*old_lockdown_copy_value)(LockdownConnectionRef connection, CFStringRef domain, CFStringRef key); CFPropertyListRef new_lockdown_copy_value(LockdownConnectionRef connection, CFStringRef domain, CFStringRef Key) { CFPropertyListRef Ret = old_lockdown_copy_value(connection, domain, Key); NSLog (@"LDHooker:%@\nReturn Value:%@" , Key, Ret); return old_lockdown_copy_value(connection, domain, Key); } % ctor { void *SymbolGN = MSFindSymbol(MSGetImageByName("/System/Library/Frameworks/IOKit.framework/IOKit" ), "_IORegistryEntryGetName" ); NSLog (@"GName: %p" , SymbolGN); MSHookFunction((void *)SymbolGN, (void *)newIORegistryEntryGetName, (void **)&oldIORegistryEntryGetName); void *SymbolSC = MSFindSymbol(MSGetImageByName("/System/Library/Frameworks/IOKit.framework/IOKit" ), "_IORegistryEntrySearchCFProperty" ); NSLog (@"SPropertey: %p" , SymbolSC); MSHookFunction( (void *)SymbolSC, (void *)newIORegistryEntrySearchCFProperty, (void **)&oldIORegistryEntrySearchCFProperty); } else { NSLog (@"MGHooker: CSE Failed" ); } }
其实我想大家应该猜到我下面想做什么了,既然都已经对守护进程下手了,要不干脆我们自己也开一个守护进程的了,加个 root 权限,对所有其他进程安装钩子,如果调用了 Device ID 相关的 API,把返回值魔改掉,岂不美滋滋!代码如下:
1 2 3 4 5 6 7 8 9 #import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { NSLog (@"想不到吧,这次我真的编不出来了😂" ); } return 0 ; }
那么今天的代码就写到这里了,下台鞠躬!
注:以上所有代码全是瞎掰,如能运行,纯属巧合。
如何实现 AppStore App 的自动下载
Hooking MGCopyAnswer Like A Boss
------ 本文结束 ------
本作品采用采用[知识共享署名 4.0 国际许可协议](https://creativecommons.org/licenses/by/4.0/deed.zh)进行许可。
donate
感谢鼓励